/*
    libhdvbd - Library for building sophisticated DVB applications
    Copyright (C) 2012 Tony Houghton <h@realh.co.uk>

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation; either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

/* High-level handling of section filters */

#include <glib.h>

#include <errno.h>
#include <string.h>

#include <unistd.h>
#include <linux/dvb/dmx.h>
#include <sys/ioctl.h>
#include <sys/poll.h>

#include "secfilter.h"

#define HDVB_SECTION_FILTER_THREAD_NAME "section-filter"

typedef struct {
    HdvbSectionFilter *sctf;
    HdvbSection *section;
    int reason;
} _HdvbSectionFilterClosure;

static void
hdvb_section_filter_finalize(HdvbSectionFilter *sctf)
{
    if (sctf->sections)
    {
        g_hash_table_remove_all(sctf->sections);
        g_hash_table_unref(sctf->sections);
        sctf->sections = NULL;
    }
    if (sctf->has_lock)
    {
        g_mutex_clear(&sctf->lock);
        sctf->has_lock = FALSE;
    }
    g_free(sctf);
}

/* Call while locked or with has_lock FALSE.
 * Lock will be released whether sctf is finalized or not.
 */
static void
hdvb_section_filter_unref(HdvbSectionFilter *sctf)
{
    GMutex *lock = &sctf->lock;

    if (!--sctf->ref)
    {
        hdvb_section_filter_finalize(sctf);
        if (sctf->has_lock)
        {
            g_mutex_unlock(lock);
            g_mutex_clear(lock);
            sctf->has_lock = FALSE;
        }
    }
    else
    {
        if (sctf->has_lock)
            g_mutex_unlock(lock);
    }
}

static gboolean
hdvb_section_filter_idle_cb(_HdvbSectionFilterClosure *closure)
{
    HdvbSectionFilter *sctf = closure->sctf;
    GMutex *lock = &sctf->lock;

    /* Call client callback before acquiring lock otherwise a long
     * callback would block read thread. Possible thread safety issues
     * here, but should be OK.
     */
    if (sctf->running)
    {
        sctf->callback(sctf->cb_handle,
                closure->reason, closure->section, sctf);
    }
    g_mutex_lock(lock);
    hdvb_section_filter_unref(sctf);
    g_free(closure);
    return FALSE;
}

static void
hdvb_section_filter_queue_callback(HdvbSectionFilter *sctf,
        HdvbSection *section, int reason)
{
    _HdvbSectionFilterClosure *closure = g_new(_HdvbSectionFilterClosure, 1);

    closure->sctf = sctf;
    closure->section = section;
    closure->reason = reason;
    ++sctf->ref;
    g_idle_add((GSourceFunc) hdvb_section_filter_idle_cb, closure);
}

static void
hdvb_section_filter_read(HdvbSectionFilter *sctf)
{
    ssize_t sect_size = sctf->xl ? 4096 : 1024;
    HdvbSection *section = g_slice_alloc(sect_size);
    sect_size = read(sctf->fd, section, sect_size);
    gboolean do_cb = !sctf->sections;

    if (sect_size <= 0)
    {
        int e = errno;

        g_slice_free1(sctf->xl ? 4096 : 1024, section);
        if (!e)
            e = EBADF;
        hdvb_section_filter_queue_callback(sctf, NULL, e);
    }
    if (sctf->sections && HDVB_SECTION_CURRENT_NEXT(section))
    {
        int key = HDVB_SECTION_SDT_TS_ID(section);
        HdvbSection **secs = g_hash_table_lookup(sctf->sections,
                GINT_TO_POINTER(key));
        int v = HDVB_SECTION_VERSION(section);
        int old_v;

        if (!secs)
        {
            secs = g_new0(HdvbSection *, section->h.last_section_number + 1);
            old_v = v;
            g_hash_table_insert(sctf->sections, GINT_TO_POINTER(key), secs);
        }
        else
        {
            int n;

            for (n = 0; ; ++n)
            {
                if (secs[n])
                {
                    old_v = HDVB_SECTION_VERSION(secs[n]);
                    break;
                }
            }
        }
        if (v > old_v || (!v && old_v >= 16))
        {
            /* Appear to be receiving new version, discard all old ones */
            int n;
            int l = 255;

            for (n = 0; n <= l; ++n)
            {
                if (!secs[n])
                    continue;
                l = secs[n]->h.last_section_number;
                hdvb_section_free(secs[n]);
                secs[n] = NULL;
            }
            if (l != section->h.last_section_number + 1)
            {
                g_free(secs);
                secs = g_new0(HdvbSection *,
                        section->h.last_section_number + 1);
                g_hash_table_replace(sctf->sections,
                        GINT_TO_POINTER(key), secs);
            }
            old_v = v;
            if (sctf->multi_id)
                sctf->got_repeat = FALSE;
        }
        if (v == old_v)
        {
            if (!secs[section->h.section_number])
            {
                secs[section->h.section_number] = section;
                do_cb = TRUE;
            }
            else
            {
                /* This helps us tell whether harvesting is complete */
                sctf->got_repeat = TRUE;
                hdvb_section_filter_queue_callback(sctf, NULL, 0);
            }
        }
    }
    if (do_cb && (sctf->include_next || HDVB_SECTION_CURRENT_NEXT(section)))
    {
        hdvb_section_filter_queue_callback(sctf, section, 0);
    }
    if (!do_cb)
        hdvb_section_free(section);
}

static gpointer
hdvb_section_filter_thread(gpointer thread_handle)
{
    HdvbSectionFilter *sctf = thread_handle;
    struct pollfd poll_str;
    GMutex *lock = &sctf->lock;

    g_mutex_lock(lock);
    poll_str.fd = sctf->fd;
    poll_str.events = POLLIN;
    poll_str.revents = 0;
    while (sctf->running)
    {
        int result;

        g_mutex_unlock(lock);
        result = poll(&poll_str, 1, sctf->timeout);
        g_mutex_lock(lock);
        if (!sctf->running)
            break;
        if (result <= 0)
        {
            hdvb_section_filter_queue_callback(sctf, NULL,
                    result ? errno : ETIMEDOUT);
        }
        else
        {
            hdvb_section_filter_read(sctf);
        }
    }
    hdvb_section_filter_unref(sctf);
    return NULL;
}

static void
free_sec_array(gpointer data)
{
    HdvbSection **secs = data;
    int n;
    int l = 255;

    for (n = 0; n <= l; ++n)
    {
        if (secs[n])
        {
            l = secs[n]->h.last_section_number;
            hdvb_section_free(secs[n]);
        }
    }
    g_free(secs);
}

HdvbSectionFilter *
hdvb_section_filter_new_named(HdvbReceiver *rcv, const char *thread_name,
        int pid, guint8 table_id, guint8 table_id_mask,
        guint16 section_id, guint16 section_id_mask,
        int timeout,
        HdvbSectionFilterCallback callback,
        gboolean harvest, gboolean include_next, gboolean multi_id,
        gpointer handle)
{
    HdvbSectionFilter *sctf = g_new0(HdvbSectionFilter, 1);
    struct dmx_sct_filter_params params;

    sctf->has_lock = FALSE;
    sctf->rcv = rcv;
    sctf->include_next = include_next;
    sctf->table_id = table_id;
    sctf->pid = pid;
    sctf->callback = callback;
    sctf->cb_handle = handle;
    sctf->ref = 2;  /* One for master thread, one for read thread */
    /* EIT sections may be longer than standard */
    sctf->xl = HDVB_TABLE_ID_IS_EIT(table_id);
    if (harvest)
    {
        sctf->sections = g_hash_table_new_full(g_direct_hash, g_direct_equal,
                NULL, free_sec_array);
        sctf->multi_id = multi_id;
        sctf->got_repeat = !multi_id;
    }

    memset(&params, 0, sizeof(params));
    params.pid = pid;
    params.filter.filter[0] = table_id;
    params.filter.filter[1] = section_id >> 8;
    params.filter.filter[2] = section_id & 0xff;
    params.filter.mask[0] = table_id_mask;
    params.filter.mask[1] = section_id_mask >> 8;
    params.filter.mask[2] = section_id_mask & 0xff;
    params.flags = DMX_CHECK_CRC | DMX_IMMEDIATE_START;

    sctf->fd = hdvb_receiver_set_section_filter(rcv, &params);
    if (sctf->fd < 0)
    {
        int result = sctf->fd;

        g_free(sctf);
        errno = -result;
        return NULL;
    }
    g_mutex_init(&sctf->lock);
    sctf->has_lock = TRUE;
    sctf->timeout = timeout;
    sctf->running = TRUE;
    g_thread_new(HDVB_SECTION_FILTER_THREAD_NAME,
            hdvb_section_filter_thread, sctf);
    return sctf;
}

static void
hdvb_section_filter_stop_internal(HdvbSectionFilter *sctf)
{
    if (sctf->running)
    {
        sctf->running = FALSE;
        ioctl(sctf->fd, DMX_STOP, NULL);
        close(sctf->fd);
        sctf->fd = -1;
    }
}

void
hdvb_section_filter_stop(HdvbSectionFilter *sctf)
{
    g_mutex_lock(&sctf->lock);
    hdvb_section_filter_stop_internal(sctf);
    g_mutex_unlock(&sctf->lock);
}

void
hdvb_section_filter_delete(HdvbSectionFilter *sctf)
{
    g_mutex_lock(&sctf->lock);
    hdvb_section_filter_stop_internal(sctf);
    hdvb_section_filter_unref(sctf);
}

gboolean
hdvb_section_filter_table_is_complete(HdvbSectionFilter *sctf)
{
    GList *sections, *link;
    gboolean result = TRUE;

    if (!sctf->got_repeat)
        return FALSE;
    sections = g_hash_table_get_values(sctf->sections);
    for (link = sections; link; link = g_list_next(link))
    {
        int n;
        HdvbSection **secs = link->data;

        if (!secs[0])
        {
            result = FALSE;
            break;
        }
        for (n = 0; n <= secs[0]->h.last_section_number; ++n)
        {
            if (!secs[n])
            {
                result = FALSE;
                break;
            }
        }
        if (!result)
            break;
    }
    g_list_free(sections);
    return result;
}

GList *
hdvb_section_filter_list_sections(HdvbSectionFilter *sctf)
{
    GList *sections, *link;
    GList *result = NULL;

    sections = g_hash_table_get_values(sctf->sections);
    for (link = sections; link; link = g_list_next(link))
    {
        int n;
        HdvbSection **secs = link->data;
        int l = 255;

        for (n = 0; n <= l; ++n)
        {
            if (secs[n])
            {
                l = secs[n]->h.last_section_number;
                result = g_list_append(result, secs[n]);
            }
        }
    }
    g_list_free(sections);
    return result;
}
